判断零值
在 go 语言中,区分一个字段的值是零值还是赋予和零值相同的值的方法有:
- 使用结构体,判断是否为 nil 来判断。如果为 nil,则是零值。
- 使用指针的方式,还是判断是否为 nil 来判断。如果为 nil,则是零值。
1 2 3 4
| type Book struct{ Price sql.NullInt64 Desc *string }
|
oneof
oneof
是Protocol Buffers中的一个关键字,用于定义一组互斥的字段。具体来说,oneof
语句会告诉编译器在这个oneof
语句所在的message中,只能同时存在这些字段中的一个。
例如,我们可以如下定义一个带有oneof
字段的message:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| syntax = "proto3";
option go_package="oneof/pb";
package oneof;
message MyMessage { oneof foo { int32 option1 = 1; string option2 = 2; bool option3 = 3; } }
// protoc --proto_path=pb --go_out=pb --go_opt=paths=source_relative oneof.proto
|
上述代码中,我们定义了一个MyMessage
消息,其中包含了一个foo
字段,它被定义成了一个oneof
类型,包含了三个可能的选项:option1
、option2
和option3
。 这意味着,在这个消息中,只能同时出现option1
、 option2
或option3
中的一个,并且任何时候只有一个选项是有效的。
使用oneof
可以使消息更加紧凑和易于处理,因为只需要考虑每个oneof
中的一个字段,而不需要处理其他的字段。
客户端使用 oneof
在使用了 protoc 命令生成 go 文件后,将 oneof 将字段生成了MyMessage_Option1,MyMessage_Option2,MyMessage_Option3 类似的选项。创建 MyMessage 的时候,直接选取一个创建即可。
1 2 3 4 5 6 7
|
message1 := pb.MyMessage{Foo: &pb.MyMessage_Option3{Option3: false}}
message2 := pb.MyMessage{Foo: &pb.MyMessage_Option1{Option1: 100}} fmt.Println(message1, message2)
|
服务端使用 oneof
服务端接收到了 MyMessage 后,需要对 Foo 字段进行断言处理。
1 2 3 4 5 6 7 8 9
| switch v := message1.Foo.(type) { case *pb.MyMessage_Option1: fmt.Printf("用户 Foo 字段传递了 Option1, %v", v) case *pb.MyMessage_Option2: fmt.Printf("用户 Foo 字段传递了 Option2, %v", v) case *pb.MyMessage_Option3: fmt.Printf("用户 Foo 字段传递了 Option3, %v", v) }
|
完整示例
完整代码可参考:https://github.com/rexyan/Go-Microservice/tree/main/oneof
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package main
import ( "fmt" "oneof/pb" )
func main() {
message1 := pb.MyMessage{Foo: &pb.MyMessage_Option3{Option3: false}} message2 := pb.MyMessage{Foo: &pb.MyMessage_Option1{Option1: 100}} fmt.Println(message1, message2)
switch v := message1.Foo.(type) { case *pb.MyMessage_Option1: fmt.Printf("用户 Foo 字段传递了 Option1, %v", v) case *pb.MyMessage_Option2: fmt.Printf("用户 Foo 字段传递了 Option2, %v", v) case *pb.MyMessage_Option3: fmt.Printf("用户 Foo 字段传递了 Option3, %v", v) } }
|
WrapValue
完整代码可参考:https://github.com/rexyan/Go-Microservice/tree/main/WrapValue
protobuf v3在删除required
的同时把optional
也一起删除了(v3.15.0又加回来了),这使得我们没办法轻易判断某些字段究竟是未赋值还是其被赋值为零值。
例如,下面示例中,当book.Price = 0
时我们没办法区分book.Price
字段是未赋值还是被赋值为0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| syntax = "proto3";
option go_package="WrapValue/pb";
package WrapValue;
import "google/protobuf/wrappers.proto";
message Book { string title = 1; string author = 2; google.protobuf.Int64Value price = 3; }
// protoc --proto_path=pb --go_out=pb --go_opt=paths=source_relative WrapValue.proto
|
类似这种场景推荐使用google/protobuf/wrappers.proto
中定义的 WrapValue,本质上就是使用自定义 message 代替基本类型。从而通过判断是否为 nil 来判断是零值还是赋值和零值相等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package main
import ( "WrapValue/pb" "fmt" "google.golang.org/protobuf/types/known/wrapperspb" )
func main() { book := pb.Book{ Title: "《说明》", Author: "张三", Price: &wrapperspb.Int64Value{Value: 9900}, }
// 服务端解析值 // 判断 Price 封装的类型是否为 nil,来区分 Price 是零值还是没有值。 if book.GetPrice() != nil { fmt.Println(book.GetPrice().GetValue()) } }
|
以上是针对没有 optional 字段的时候的解决方法,当 protobuf 版本大于 v3.15.0 时,以上场景,我们可以使用 optional 字段来解决。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| syntax = "proto3";
option go_package="WrapValue/pb";
package WrapValue;
import "google/protobuf/wrappers.proto";
message Book { string title = 1; string author = 2; google.protobuf.Int64Value price = 3; }
message NewBook { string title = 1; string author = 2; optional int64 price = 3; }
// protoc --proto_path=pb --go_out=pb --go_opt=paths=source_relative WrapValue.proto
|
使用 optional 和使用 WrapValue 类似的方法,WrapValue 是使用封装一种类型来判断,而 optional 是使用指针来判断。两者都是判断是否为 nil,来区分是否是零值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| package main
import ( "WrapValue/pb" "fmt" "google.golang.org/protobuf/types/known/wrapperspb" )
func main() { book := pb.Book{ Title: "《说明》", Author: "张三", Price: &wrapperspb.Int64Value{Value: 9900}, }
if book.GetPrice() != nil { fmt.Println(book.GetPrice().GetValue()) }
a := int64(100) newBook := pb.NewBook{ Title: "《说明》", Author: "张三", Price: &a, }
if newBook.Price != nil { fmt.Println(newBook.GetPrice()) } }
|
FieldMask
完整代码可参考:https://github.com/rexyan/Go-Microservice/tree/main/FieldMask
假设现在需要实现一个更新书籍信息接口,但是如果我们的Book
中定义有很多很多字段时,我们不太可能每次请求都去全量更新Book
的每个字段,因为通常每次操作只会更新1到2个字段。
那么我们该如何确定每次更新操作涉及到了哪些具体字段呢?答案是使用google/protobuf/field_mask.proto
,它能够记录在一次更新请求中涉及到的具体字段路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| syntax = "proto3";
option go_package="FieldMask/pb"; package FieldMask;
import "google/protobuf/field_mask.proto";
message Book{ string title=1; int64 price=2; }
// 更新 book 信息传递的消息 message UpdateBookRequest { // 操作人 string op = 1; // 要更新的书籍信息 Book book = 2;
// 记录要更新的字段 google.protobuf.FieldMask update_mask = 3; }
// protoc --proto_path=pb --go_out=pb --go_opt=paths=source_relative hello.proto
|
在客户端发送更新请求的时候,使用 &fieldmaskpb.FieldMask{Paths: paths}
来传递要更新的字段信息。服务端借用第三方工具将 FieldMask 解析为具体的 go 的数据,例如下面中的 map
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| package main
import ( "FieldMask/pb" "fmt" "github.com/iancoleman/strcase" fieldMaskUtils "github.com/mennanov/fieldmask-utils" "google.golang.org/protobuf/types/known/fieldmaskpb" )
func main() { paths := []string{"price"}
updateBook := pb.UpdateBookRequest{ Op: "操作人", Book: &pb.Book{ Title: "《示例设计》", Price: 999, }, UpdateMask: &fieldmaskpb.FieldMask{Paths: paths}, }
mask, err := fieldMaskUtils.MaskFromProtoFieldMask(updateBook.UpdateMask, strcase.ToCamel) if err != nil { return } dst := make(map[string]interface{}) err = fieldMaskUtils.StructToMap(mask, updateBook.Book, dst) if err != nil { return } fmt.Println(dst) }
|